Skip to main content
Glama

basic-memory

SPEC-13 CLI Authentication with Subscription Validation.md36.4 kB
--- title: 'SPEC-13: CLI Authentication with Subscription Validation' type: spec permalink: specs/spec-12-cli-auth-subscription-validation tags: - authentication - security - cli - subscription status: draft created: 2025-10-02 --- # SPEC-13: CLI Authentication with Subscription Validation ## Why The Basic Memory Cloud CLI currently has a security gap in authentication that allows unauthorized access: **Current Web Flow (Secure)**: 1. User signs up via WorkOS AuthKit 2. User creates Polar subscription 3. Web app validates subscription before calling `POST /tenants/setup` 4. Tenant provisioned only after subscription validation ✅ **Current CLI Flow (Insecure)**: 1. User signs up via WorkOS AuthKit (OAuth device flow) 2. User runs `bm cloud login` 3. CLI receives JWT token from WorkOS 4. CLI can access all cloud endpoints without subscription check ❌ **Problem**: Anyone can sign up with WorkOS and immediately access cloud infrastructure via CLI without having an active Polar subscription. This creates: - Revenue loss (free resource consumption) - Security risk (unauthorized data access) - Support burden (users accessing features they haven't paid for) **Root Cause**: The CLI authentication flow validates JWT tokens but doesn't verify subscription status before granting access to cloud resources. ## What Add subscription validation to authentication flow to ensure only users with active Polar subscriptions can access cloud resources across all access methods (CLI, MCP, Web App, Direct API). **Affected Components**: ### basic-memory-cloud (Cloud Service) - `apps/cloud/src/basic_memory_cloud/deps.py` - Add subscription validation dependency - `apps/cloud/src/basic_memory_cloud/services/subscription_service.py` - Add subscription check method - `apps/cloud/src/basic_memory_cloud/api/tenant_mount.py` - Protect mount endpoints - `apps/cloud/src/basic_memory_cloud/api/proxy.py` - Protect proxy endpoints ### basic-memory (CLI) - `src/basic_memory/cli/commands/cloud/core_commands.py` - Handle 403 errors - `src/basic_memory/cli/commands/cloud/api_client.py` - Parse subscription errors - `docs/cloud-cli.md` - Document subscription requirement **Endpoints to Protect**: - `GET /tenant/mount/info` - Used by CLI bisync setup - `POST /tenant/mount/credentials` - Used by CLI bisync credentials - `GET /proxy/{path:path}` - Used by Web App, MCP tools, CLI tools, Direct API - All other `/proxy/*` endpoints - Centralized access point for all user operations ## Complete Authentication Flow Analysis ### Overview of All Access Flows Basic Memory Cloud has **7 distinct authentication flows**. This spec closes subscription validation gaps in flows 2-4 and 6, which all converge on the `/proxy/*` endpoints. ### Flow 1: Polar Webhook → Registration ✅ SECURE ``` Polar webhook → POST /api/webhooks/polar → Validates Polar webhook signature → Creates/updates subscription in database → No direct user access - webhook only ``` **Auth**: Polar webhook signature validation **Subscription Check**: N/A (webhook creates subscriptions) **Status**: ✅ Secure - webhook validated, no user JWT involved ### Flow 2: Web App Login ❌ NEEDS FIX ``` User → apps/web (Vue.js/Nuxt) → WorkOS AuthKit magic link authentication → JWT stored in browser session → Web app calls /proxy/{project}/... endpoints (memory, directory, projects) → proxy.py validates JWT but does NOT check subscription → Access granted without subscription ❌ ``` **Auth**: WorkOS JWT via `CurrentUserProfileHybridJwtDep` **Subscription Check**: ❌ Missing **Fixed By**: Task 1.4 (protect `/proxy/*` endpoints) ### Flow 3: MCP (Model Context Protocol) ❌ NEEDS FIX ``` AI Agent (Claude, Cursor, etc.) → https://mcp.basicmemory.com → AuthKit OAuth device flow → JWT stored in AI agent → MCP tools call {cloud_host}/proxy/{endpoint} with Authorization header → proxy.py validates JWT but does NOT check subscription → MCP tools can access all cloud resources without subscription ❌ ``` **Auth**: AuthKit JWT via `CurrentUserProfileHybridJwtDep` **Subscription Check**: ❌ Missing **Fixed By**: Task 1.4 (protect `/proxy/*` endpoints) ### Flow 4: CLI Auth (basic-memory) ❌ NEEDS FIX ``` User → bm cloud login → AuthKit OAuth device flow → JWT stored in ~/.basic-memory/tokens.json → CLI calls: - {cloud_host}/tenant/mount/info (for bisync setup) - {cloud_host}/tenant/mount/credentials (for bisync credentials) - {cloud_host}/proxy/{endpoint} (for all MCP tools) → tenant_mount.py and proxy.py validate JWT but do NOT check subscription → Access granted without subscription ❌ ``` **Auth**: AuthKit JWT via `CurrentUserProfileHybridJwtDep` **Subscription Check**: ❌ Missing **Fixed By**: Task 1.3 (protect `/tenant/mount/*`) + Task 1.4 (protect `/proxy/*`) ### Flow 5: Cloud CLI (Admin Tasks) ✅ SECURE ``` Admin → python -m basic_memory_cloud.cli.tenant_cli → Uses CLIAuth with admin WorkOS OAuth client → Gets JWT token with admin org membership → Calls /tenants/* endpoints (create, list, delete tenants) → tenants.py validates JWT AND admin org membership via AdminUserHybridDep → Access granted only to admin organization members ✅ ``` **Auth**: AuthKit JWT + Admin org validation via `AdminUserHybridDep` **Subscription Check**: N/A (admins bypass subscription requirement) **Status**: ✅ Secure - admin-only endpoints, separate from user flows ### Flow 6: Direct API Calls ❌ NEEDS FIX ``` Any HTTP client → {cloud_host}/proxy/{endpoint} → Sends Authorization: Bearer {jwt} header → proxy.py validates JWT but does NOT check subscription → Direct API access without subscription ❌ ``` **Auth**: WorkOS or AuthKit JWT via `CurrentUserProfileHybridJwtDep` **Subscription Check**: ❌ Missing **Fixed By**: Task 1.4 (protect `/proxy/*` endpoints) ### Flow 7: Tenant API Instance (Internal) ✅ SECURE ``` /proxy/* → Tenant API (basic-memory-{tenant_id}.fly.dev) → Validates signed header from proxy (tenant_id + signature) → Direct external access will be disabled in production → Only accessible via /proxy endpoints ``` **Auth**: Signed header validation from proxy **Subscription Check**: N/A (internal only, validated at proxy layer) **Status**: ✅ Secure - validates proxy signature, not directly accessible ### Authentication Flow Summary Matrix | Flow | Access Method | Current Auth | Subscription Check | Fixed By SPEC-13 | |------|---------------|--------------|-------------------|------------------| | 1. Polar Webhook | Polar webhook → `/api/webhooks/polar` | Polar signature | N/A (webhook) | N/A | | 2. Web App | Browser → `/proxy/*` | WorkOS JWT ✅ | ❌ Missing | ✅ Task 1.4 | | 3. MCP | AI Agent → `/proxy/*` | AuthKit JWT ✅ | ❌ Missing | ✅ Task 1.4 | | 4. CLI | `bm cloud` → `/tenant/mount/*` + `/proxy/*` | AuthKit JWT ✅ | ❌ Missing | ✅ Task 1.3 + 1.4 | | 5. Cloud CLI (Admin) | `tenant_cli` → `/tenants/*` | AuthKit JWT ✅ + Admin org | N/A (admin) | N/A (admin bypass) | | 6. Direct API | HTTP client → `/proxy/*` | WorkOS/AuthKit JWT ✅ | ❌ Missing | ✅ Task 1.4 | | 7. Tenant API | Proxy → tenant instance | Proxy signature ✅ | N/A (internal) | N/A | ### Key Insights 1. **Single Point of Failure**: All user access (Web, MCP, CLI, Direct API) converges on `/proxy/*` endpoints 2. **Centralized Fix**: Protecting `/proxy/*` with subscription validation closes gaps in flows 2, 3, 4, and 6 simultaneously 3. **Admin Bypass**: Cloud CLI admin tasks use separate `/tenants/*` endpoints with admin-only access (no subscription needed) 4. **Defense in Depth**: `/tenant/mount/*` endpoints also protected for CLI bisync operations ### Architecture Benefits The `/proxy` layer serves as the **single centralized authorization point** for all user access: - ✅ One place to validate JWT tokens - ✅ One place to check subscription status - ✅ One place to handle tenant routing - ✅ Protects Web App, MCP, CLI, and Direct API simultaneously This architecture makes the fix comprehensive and maintainable. ## How (High Level) ### Option A: Database Subscription Check (Recommended) **Approach**: Add FastAPI dependency that validates subscription status from database before allowing access. **Implementation**: 1. **Create Subscription Validation Dependency** (`deps.py`) ```python async def get_authorized_cli_user_profile( credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)], session: DatabaseSessionDep, user_profile_repo: UserProfileRepositoryDep, subscription_service: SubscriptionServiceDep, ) -> UserProfile: """ Hybrid authentication with subscription validation for CLI access. Validates JWT (WorkOS or AuthKit) and checks for active subscription. Returns UserProfile if both checks pass. """ # Try WorkOS JWT first (faster validation path) try: user_context = await validate_workos_jwt(credentials.credentials) except HTTPException: # Fall back to AuthKit JWT validation try: user_context = await validate_authkit_jwt(credentials.credentials) except HTTPException as e: raise HTTPException( status_code=401, detail="Invalid JWT token. Authentication required.", ) from e # Check subscription status has_subscription = await subscription_service.check_user_has_active_subscription( session, user_context.workos_user_id ) if not has_subscription: raise HTTPException( status_code=403, detail={ "error": "subscription_required", "message": "Active subscription required for CLI access", "subscribe_url": "https://basicmemory.com/subscribe" } ) # Look up and return user profile user_profile = await user_profile_repo.get_user_profile_by_workos_user_id( session, user_context.workos_user_id ) if not user_profile: raise HTTPException(401, detail="User profile not found") return user_profile ``` ```python AuthorizedCLIUserProfileDep = Annotated[UserProfile, Depends(get_authorized_cli_user_profile)] ``` 2. **Add Subscription Check Method** (`subscription_service.py`) ```python async def check_user_has_active_subscription( self, session: AsyncSession, workos_user_id: str ) -> bool: """Check if user has active subscription.""" # Use existing repository method to get subscription by workos_user_id # This joins UserProfile -> Subscription in a single query subscription = await self.subscription_repository.get_subscription_by_workos_user_id( session, workos_user_id ) return subscription is not None and subscription.status == "active" ``` 3. **Protect Endpoints** (Replace `CurrentUserProfileHybridJwtDep` with `AuthorizedCLIUserProfileDep`) ```python # Before @router.get("/mount/info") async def get_mount_info( user_profile: CurrentUserProfileHybridJwtDep, session: DatabaseSessionDep, ): tenant_id = user_profile.tenant_id ... # After @router.get("/mount/info") async def get_mount_info( user_profile: AuthorizedCLIUserProfileDep, # Now includes subscription check session: DatabaseSessionDep, ): tenant_id = user_profile.tenant_id # No changes needed to endpoint logic ... ``` 4. **Update CLI Error Handling** ```python # In core_commands.py login() try: success = await auth.login() if success: # Test subscription by calling protected endpoint await make_api_request("GET", f"{host_url}/tenant/mount/info") except CloudAPIError as e: if e.status_code == 403 and e.detail.get("error") == "subscription_required": console.print("[red]Subscription required[/red]") console.print(f"Subscribe at: {e.detail['subscribe_url']}") raise typer.Exit(1) ``` **Pros**: - Simple to implement - Fast (single database query) - Clear error messages - Works with existing subscription flow **Cons**: - Database is source of truth (could get out of sync with Polar) - Adds one extra subscription lookup query per request (lightweight JOIN query) ### Option B: WorkOS Organizations **Approach**: Add users to "beta-users" organization in WorkOS after subscription creation, validate org membership via JWT claims. **Implementation**: 1. After Polar subscription webhook, add user to WorkOS org via API 2. Validate `org_id` claim in JWT matches authorized org 3. Use existing `get_admin_workos_jwt` pattern **Pros**: - WorkOS as single source of truth - No database queries needed - More secure (harder to bypass) **Cons**: - More complex (requires WorkOS API integration) - Requires managing WorkOS org membership - Less control over error messages - Additional API calls during registration ### Recommendation **Start with Option A (Database Check)** for: - Faster implementation - Clearer error messages - Easier testing - Existing subscription infrastructure **Consider Option B later** if: - Need tighter security - Want to reduce database dependency - Scale requires fewer database queries ## How to Evaluate ### Success Criteria **1. Unauthorized Users Blocked** - [ ] User without subscription cannot complete `bm cloud login` - [ ] User without subscription receives clear error with subscribe link - [ ] User without subscription cannot run `bm cloud setup` - [ ] User without subscription cannot run `bm sync` in cloud mode **2. Authorized Users Work** - [ ] User with active subscription can login successfully - [ ] User with active subscription can setup bisync - [ ] User with active subscription can sync files - [ ] User with active subscription can use all MCP tools via proxy **3. Subscription State Changes** - [ ] Expired subscription blocks access with clear error - [ ] Renewed subscription immediately restores access - [ ] Cancelled subscription blocks access after grace period **4. Error Messages** - [ ] 403 errors include "subscription_required" error code - [ ] Error messages include subscribe URL - [ ] CLI displays user-friendly messages - [ ] Errors logged appropriately for debugging **5. No Regressions** - [ ] Web app login/subscription flow unaffected - [ ] Admin endpoints still work (bypass check) - [ ] Tenant provisioning workflow unchanged - [ ] Performance not degraded ### Test Cases **Manual Testing**: ```bash # Test 1: Unauthorized user 1. Create new WorkOS account (no subscription) 2. Run `bm cloud login` 3. Verify: Login succeeds but shows subscription required error 4. Verify: Cannot run `bm cloud setup` 5. Verify: Clear error message with subscribe link # Test 2: Authorized user 1. Use account with active Polar subscription 2. Run `bm cloud login` 3. Verify: Login succeeds without errors 4. Run `bm cloud setup` 5. Verify: Setup completes successfully 6. Run `bm sync` 7. Verify: Sync works normally # Test 3: Subscription expiration 1. Use account with active subscription 2. Manually expire subscription in database 3. Run `bm cloud login` 4. Verify: Blocked with clear error 5. Renew subscription 6. Run `bm cloud login` again 7. Verify: Access restored ``` **Automated Tests**: ```python # Test subscription validation dependency async def test_authorized_user_allowed( db_session, user_profile_repo, subscription_service, mock_jwt_credentials ): # Create user with active subscription user_profile = await create_user_with_subscription(db_session, status="active") # Mock JWT credentials for the user credentials = mock_jwt_credentials(user_profile.workos_user_id) # Should not raise exception result = await get_authorized_cli_user_profile( credentials, db_session, user_profile_repo, subscription_service ) assert result.id == user_profile.id assert result.workos_user_id == user_profile.workos_user_id async def test_unauthorized_user_blocked( db_session, user_profile_repo, subscription_service, mock_jwt_credentials ): # Create user without subscription user_profile = await create_user_without_subscription(db_session) credentials = mock_jwt_credentials(user_profile.workos_user_id) # Should raise 403 with pytest.raises(HTTPException) as exc: await get_authorized_cli_user_profile( credentials, db_session, user_profile_repo, subscription_service ) assert exc.value.status_code == 403 assert exc.value.detail["error"] == "subscription_required" async def test_inactive_subscription_blocked( db_session, user_profile_repo, subscription_service, mock_jwt_credentials ): # Create user with cancelled/inactive subscription user_profile = await create_user_with_subscription(db_session, status="cancelled") credentials = mock_jwt_credentials(user_profile.workos_user_id) # Should raise 403 with pytest.raises(HTTPException) as exc: await get_authorized_cli_user_profile( credentials, db_session, user_profile_repo, subscription_service ) assert exc.value.status_code == 403 assert exc.value.detail["error"] == "subscription_required" ``` ## Implementation Tasks ### Phase 1: Cloud Service (basic-memory-cloud) #### Task 1.1: Add subscription check method to SubscriptionService ✅ **File**: `apps/cloud/src/basic_memory_cloud/services/subscription_service.py` - [x] Add method `check_subscription(session: AsyncSession, workos_user_id: str) -> bool` - [x] Use existing `self.subscription_repository.get_subscription_by_workos_user_id(session, workos_user_id)` - [x] Check both `status == "active"` AND `current_period_end >= now()` - [x] Log both values when check fails - [x] Add docstring explaining the method - [x] Run `just typecheck` to verify types **Actual implementation**: ```python async def check_subscription( self, session: AsyncSession, workos_user_id: str ) -> bool: """Check if user has active subscription with valid period.""" subscription = await self.subscription_repository.get_subscription_by_workos_user_id( session, workos_user_id ) if subscription is None: return False if subscription.status != "active": logger.warning("Subscription inactive", workos_user_id=workos_user_id, status=subscription.status, current_period_end=subscription.current_period_end) return False now = datetime.now(timezone.utc) if subscription.current_period_end is None or subscription.current_period_end < now: logger.warning("Subscription expired", workos_user_id=workos_user_id, status=subscription.status, current_period_end=subscription.current_period_end) return False return True ``` #### Task 1.2: Add subscription validation dependency ✅ **File**: `apps/cloud/src/basic_memory_cloud/deps.py` - [x] Import necessary types at top of file (if not already present) - [x] Add `authorized_user_profile()` async function - [x] Implement hybrid JWT validation (WorkOS first, AuthKit fallback) - [x] Add subscription check using `subscription_service.check_subscription()` - [x] Raise `HTTPException(403)` with structured error detail if no active subscription - [x] Look up and return `UserProfile` after validation - [x] Add `AuthorizedUserProfileDep` type annotation - [x] Use `settings.subscription_url` from config (env var) - [x] Run `just typecheck` to verify types **Expected code**: ```python async def get_authorized_cli_user_profile( credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)], session: DatabaseSessionDep, user_profile_repo: UserProfileRepositoryDep, subscription_service: SubscriptionServiceDep, ) -> UserProfile: """ Hybrid authentication with subscription validation for CLI access. Validates JWT (WorkOS or AuthKit) and checks for active subscription. Returns UserProfile if both checks pass. Raises: HTTPException(401): Invalid JWT token HTTPException(403): No active subscription """ # Try WorkOS JWT first (faster validation path) try: user_context = await validate_workos_jwt(credentials.credentials) except HTTPException: # Fall back to AuthKit JWT validation try: user_context = await validate_authkit_jwt(credentials.credentials) except HTTPException as e: raise HTTPException( status_code=401, detail="Invalid JWT token. Authentication required.", ) from e # Check subscription status has_subscription = await subscription_service.check_user_has_active_subscription( session, user_context.workos_user_id ) if not has_subscription: logger.warning( "CLI access denied: no active subscription", workos_user_id=user_context.workos_user_id, ) raise HTTPException( status_code=403, detail={ "error": "subscription_required", "message": "Active subscription required for CLI access", "subscribe_url": "https://basicmemory.com/subscribe" } ) # Look up and return user profile user_profile = await user_profile_repo.get_user_profile_by_workos_user_id( session, user_context.workos_user_id ) if not user_profile: logger.error( "User profile not found after successful auth", workos_user_id=user_context.workos_user_id, ) raise HTTPException(401, detail="User profile not found") logger.info( "CLI access granted", workos_user_id=user_context.workos_user_id, user_profile_id=str(user_profile.id), ) return user_profile AuthorizedCLIUserProfileDep = Annotated[UserProfile, Depends(get_authorized_cli_user_profile)] ``` #### Task 1.3: Protect tenant mount endpoints ✅ **File**: `apps/cloud/src/basic_memory_cloud/api/tenant_mount.py` - [x] Update import: add `AuthorizedUserProfileDep` from `..deps` - [x] Replace `user_profile: CurrentUserProfileHybridJwtDep` with `user_profile: AuthorizedUserProfileDep` in: - [x] `get_tenant_mount_info()` (line ~23) - [x] `create_tenant_mount_credentials()` (line ~88) - [x] `revoke_tenant_mount_credentials()` (line ~244) - [x] `list_tenant_mount_credentials()` (line ~326) - [x] Verify no other code changes needed (parameter name and usage stays the same) - [x] Run `just typecheck` to verify types #### Task 1.4: Protect proxy endpoints ✅ **File**: `apps/cloud/src/basic_memory_cloud/api/proxy.py` - [x] Update import: add `AuthorizedUserProfileDep` from `..deps` - [x] Replace `user_profile: CurrentUserProfileHybridJwtDep` with `user_profile: AuthorizedUserProfileDep` in: - [x] `check_tenant_health()` (line ~21) - [x] `proxy_to_tenant()` (line ~63) - [x] Verify no other code changes needed (parameter name and usage stays the same) - [x] Run `just typecheck` to verify types **Why Keep /proxy Architecture:** The proxy layer is valuable because it: 1. **Centralizes authorization** - Single place for JWT + subscription validation (closes both CLI and MCP auth gaps) 2. **Handles tenant routing** - Maps tenant_id → fly_app_name without exposing infrastructure details 3. **Abstracts infrastructure** - MCP and CLI don't need to know about Fly.io naming conventions 4. **Enables features** - Can add rate limiting, caching, request logging, etc. at proxy layer 5. **Supports both flows** - CLI tools and MCP tools both use /proxy endpoints The extra HTTP hop is minimal (< 10ms) and worth it for architectural benefits. **Performance Note:** Cloud app has Redis available - can cache subscription status to reduce database queries if needed. Initial implementation uses direct database query (simple, acceptable performance ~5-10ms). #### Task 1.5: Add unit tests for subscription service **File**: `apps/cloud/tests/services/test_subscription_service.py` (create if doesn't exist) - [ ] Create test file if it doesn't exist - [ ] Add test: `test_check_user_has_active_subscription_returns_true_for_active()` - Create user with active subscription - Call `check_user_has_active_subscription()` - Assert returns `True` - [ ] Add test: `test_check_user_has_active_subscription_returns_false_for_pending()` - Create user with pending subscription - Assert returns `False` - [ ] Add test: `test_check_user_has_active_subscription_returns_false_for_cancelled()` - Create user with cancelled subscription - Assert returns `False` - [ ] Add test: `test_check_user_has_active_subscription_returns_false_for_no_subscription()` - Create user without subscription - Assert returns `False` - [ ] Run `just test` to verify tests pass #### Task 1.6: Add integration tests for dependency **File**: `apps/cloud/tests/test_deps.py` (create if doesn't exist) - [ ] Create test file if it doesn't exist - [ ] Add fixtures for mocking JWT credentials - [ ] Add test: `test_authorized_cli_user_profile_with_active_subscription()` - Mock valid JWT + active subscription - Call dependency - Assert returns UserProfile - [ ] Add test: `test_authorized_cli_user_profile_without_subscription_raises_403()` - Mock valid JWT + no subscription - Assert raises HTTPException(403) with correct error detail - [ ] Add test: `test_authorized_cli_user_profile_with_inactive_subscription_raises_403()` - Mock valid JWT + cancelled subscription - Assert raises HTTPException(403) - [ ] Add test: `test_authorized_cli_user_profile_with_invalid_jwt_raises_401()` - Mock invalid JWT - Assert raises HTTPException(401) - [ ] Run `just test` to verify tests pass #### Task 1.7: Deploy and verify cloud service - [ ] Run `just check` to verify all quality checks pass - [ ] Commit changes with message: "feat: add subscription validation to CLI endpoints" - [ ] Deploy to preview environment: `flyctl deploy --config apps/cloud/fly.toml` - [ ] Test manually: - [ ] Call `/tenant/mount/info` with valid JWT but no subscription → expect 403 - [ ] Call `/tenant/mount/info` with valid JWT and active subscription → expect 200 - [ ] Verify error response structure matches spec ### Phase 2: CLI (basic-memory) #### Task 2.1: Review and understand CLI authentication flow **Files**: `src/basic_memory/cli/commands/cloud/` - [ ] Read `core_commands.py` to understand current login flow - [ ] Read `api_client.py` to understand current error handling - [ ] Identify where 403 errors should be caught - [ ] Identify what error messages should be displayed - [ ] Document current behavior in spec if needed #### Task 2.2: Update API client error handling **File**: `src/basic_memory/cli/commands/cloud/api_client.py` - [ ] Add custom exception class `SubscriptionRequiredError` (or similar) - [ ] Update HTTP error handling to parse 403 responses - [ ] Extract `error`, `message`, and `subscribe_url` from error detail - [ ] Raise specific exception for subscription_required errors - [ ] Run `just typecheck` in basic-memory repo to verify types #### Task 2.3: Update CLI login command error handling **File**: `src/basic_memory/cli/commands/cloud/core_commands.py` - [ ] Import the subscription error exception - [ ] Wrap login flow with try/except for subscription errors - [ ] Display user-friendly error message with rich console - [ ] Show subscribe URL prominently - [ ] Provide actionable next steps - [ ] Run `just typecheck` to verify types **Expected error handling**: ```python try: # Existing login logic success = await auth.login() if success: # Test access to protected endpoint await api_client.test_connection() except SubscriptionRequiredError as e: console.print("\n[red]✗ Subscription Required[/red]\n") console.print(f"[yellow]{e.message}[/yellow]\n") console.print(f"Subscribe at: [blue underline]{e.subscribe_url}[/blue underline]\n") console.print("[dim]Once you have an active subscription, run [bold]bm cloud login[/bold] again.[/dim]") raise typer.Exit(1) ``` #### Task 2.4: Update CLI tests **File**: `tests/cli/test_cloud_commands.py` - [ ] Add test: `test_login_without_subscription_shows_error()` - Mock 403 subscription_required response - Call login command - Assert error message displayed - Assert subscribe URL shown - [ ] Add test: `test_login_with_subscription_succeeds()` - Mock successful authentication + subscription check - Call login command - Assert success message - [ ] Run `just test` to verify tests pass #### Task 2.5: Update CLI documentation **File**: `docs/cloud-cli.md` (in basic-memory-docs repo) - [ ] Add "Prerequisites" section if not present - [ ] Document subscription requirement - [ ] Add "Troubleshooting" section - [ ] Document "Subscription Required" error - [ ] Provide subscribe URL - [ ] Add FAQ entry about subscription errors - [ ] Build docs locally to verify formatting ### Phase 3: End-to-End Testing #### Task 3.1: Create test user accounts **Prerequisites**: Access to WorkOS admin and database - [ ] Create test user WITHOUT subscription: - [ ] Sign up via WorkOS AuthKit - [ ] Get workos_user_id from database - [ ] Verify no subscription record exists - [ ] Save credentials for testing - [ ] Create test user WITH active subscription: - [ ] Sign up via WorkOS AuthKit - [ ] Create subscription via Polar or dev endpoint - [ ] Verify subscription.status = "active" in database - [ ] Save credentials for testing #### Task 3.2: Manual testing - User without subscription **Environment**: Preview/staging deployment - [ ] Run `bm cloud login` with no-subscription user - [ ] Verify: Login shows "Subscription Required" error - [ ] Verify: Subscribe URL is displayed - [ ] Verify: Cannot run `bm cloud setup` - [ ] Verify: Cannot call `/tenant/mount/info` directly via curl - [ ] Document any issues found #### Task 3.3: Manual testing - User with active subscription **Environment**: Preview/staging deployment - [ ] Run `bm cloud login` with active-subscription user - [ ] Verify: Login succeeds without errors - [ ] Verify: Can run `bm cloud setup` - [ ] Verify: Can call `/tenant/mount/info` successfully - [ ] Verify: Can call `/proxy/*` endpoints successfully - [ ] Document any issues found #### Task 3.4: Test subscription state transitions **Environment**: Preview/staging deployment + database access - [ ] Start with active subscription user - [ ] Verify: All operations work - [ ] Update subscription.status to "cancelled" in database - [ ] Verify: Login now shows "Subscription Required" error - [ ] Verify: Existing tokens are rejected with 403 - [ ] Update subscription.status back to "active" - [ ] Verify: Access restored immediately - [ ] Document any issues found #### Task 3.5: Integration test suite **File**: `apps/cloud/tests/integration/test_cli_subscription_flow.py` (create if doesn't exist) - [ ] Create integration test file - [ ] Add test: `test_cli_flow_without_subscription()` - Simulate full CLI flow without subscription - Assert 403 at appropriate points - [ ] Add test: `test_cli_flow_with_active_subscription()` - Simulate full CLI flow with active subscription - Assert all operations succeed - [ ] Add test: `test_subscription_expiration_blocks_access()` - Start with active subscription - Change status to cancelled - Assert access denied - [ ] Run tests in CI/CD pipeline - [ ] Document test coverage #### Task 3.6: Load/performance testing (optional) **Environment**: Staging environment - [ ] Test subscription check performance under load - [ ] Measure latency added by subscription check - [ ] Verify database query performance - [ ] Document any performance concerns - [ ] Optimize if needed ## Implementation Summary Checklist Use this high-level checklist to track overall progress: ### Phase 1: Cloud Service 🔄 - [x] Add subscription check method to SubscriptionService - [x] Add subscription validation dependency to deps.py - [x] Add subscription_url config (env var) - [x] Protect tenant mount endpoints (4 endpoints) - [x] Protect proxy endpoints (2 endpoints) - [ ] Add unit tests for subscription service - [ ] Add integration tests for dependency - [ ] Deploy and verify cloud service ### Phase 2: CLI Updates 🔄 - [ ] Review CLI authentication flow - [ ] Update API client error handling - [ ] Update CLI login command error handling - [ ] Add CLI tests - [ ] Update CLI documentation ### Phase 3: End-to-End Testing 🧪 - [ ] Create test user accounts - [ ] Manual testing - user without subscription - [ ] Manual testing - user with active subscription - [ ] Test subscription state transitions - [ ] Integration test suite - [ ] Load/performance testing (optional) ## Questions to Resolve ### Resolved ✅ 1. **Admin Access** - ✅ **Decision**: Admin users bypass subscription check - **Rationale**: Admin endpoints already use `AdminUserHybridDep`, which is separate from CLI user endpoints - **Implementation**: No changes needed to admin endpoints 2. **Subscription Check Implementation** - ✅ **Decision**: Use Option A (Database Check) - **Rationale**: Simpler, faster to implement, works with existing infrastructure - **Implementation**: Single JOIN query via `get_subscription_by_workos_user_id()` 3. **Dependency Return Type** - ✅ **Decision**: Return `UserProfile` (not `UserContext`) - **Rationale**: Drop-in compatibility with existing endpoints, no refactoring needed - **Implementation**: `AuthorizedCLIUserProfileDep` returns `UserProfile` ### To Be Resolved ⏳ 1. **Subscription Check Frequency** - **Options**: - Check on every API call (slower, more secure) ✅ **RECOMMENDED** - Cache subscription status (faster, risk of stale data) - Check only on login/setup (fast, but allows expired subscriptions temporarily) - **Recommendation**: Check on every call via dependency injection (simple, secure, acceptable performance) - **Impact**: ~5-10ms per request (single indexed JOIN query) 2. **Grace Period** - **Options**: - No grace period - immediate block when status != "active" ✅ **RECOMMENDED** - 7-day grace period after period_end - 14-day grace period after period_end - **Recommendation**: No grace period initially, add later if needed based on customer feedback - **Implementation**: Check `subscription.status == "active"` only (ignore period_end initially) 3. **Subscription Expiration Handling** - **Question**: Should we check `current_period_end < now()` in addition to `status == "active"`? - **Options**: - Only check status field (rely on Polar webhooks to update status) ✅ **RECOMMENDED** - Check both status and current_period_end (more defensive) - **Recommendation**: Only check status field, assume Polar webhooks keep it current - **Risk**: If webhooks fail, expired subscriptions might retain access until webhook succeeds 4. **Subscribe URL** - **Question**: What's the actual subscription URL? - **Current**: Spec uses `https://basicmemory.com/subscribe` - **Action Required**: Verify correct URL before implementation 5. **Dev Mode / Testing Bypass** - **Question**: Support bypass for development/testing? - **Options**: - Environment variable: `DISABLE_SUBSCRIPTION_CHECK=true` - Always enforce (more realistic testing) ✅ **RECOMMENDED** - **Recommendation**: No bypass - use test users with real subscriptions for realistic testing - **Implementation**: Create dev endpoint to activate subscriptions for testing ## Related Specs - SPEC-9: Multi-Project Bidirectional Sync Architecture (CLI affected by this change) - SPEC-8: TigrisFS Integration (Mount endpoints protected) ## Notes - This spec prioritizes security over convenience - better to block unauthorized access than risk revenue loss - Clear error messages are critical - users should understand why they're blocked and how to resolve it - Consider adding telemetry to track subscription_required errors for monitoring signup conversion

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/basicmachines-co/basic-memory'

If you have feedback or need assistance with the MCP directory API, please join our Discord server